β

Как стать автором
Погнали в космос?
Облачный турнир на ловкость и харды

Моя лентаВсе потоки

crackidocky 4 июн 2023 в 23:12

О массивах и слайсах в Go

Средний

6 мин

46K

Go*

Туториал

Массивы в Go являлись для меня одной из сложных тем, так как я не понимал как они работают. В данной статье рассмотрим как же именно работают слайсы и массивы в Go, а также как именно работает append и copy.

Массивы

Массивы - коллекция элементов одного типа. Длина массива не может изменяться. Вот как мы можем создать массив в Go:

arr := [4]int{3,2,5,4}

Если мы создадим два массива в Go с разными длинами, то два массива будут иметь разные типы, так как длина массива в Go, входит в его тип:

a := [3]int{}
b := [2]int{}

// (a) [2]int и (b) [3]int - разные типы

Более того, если нам лень писать длину массива, то мы можем сказать компилятору, чтобы он сам подсчитал длину:

a := [...]int{1, 2, 3} // [3]int

Передача по значению

Переменная, которую мы инициализировали со значением массива, содержит именно значения массива, а не ссылку на первый элемент массива (как это сделано в C).

Именно поэтому массив в Go является примитивным типом данных, он может копироваться при передаче в другую переменную. По умолчанию в Go все значения копируются, а не передаются с помощью ссылки. Это значит, что если мы передадим наш массив в функцию, то Go скопирует данный массив и в функции будет находиться уже совершенно другой массив (вернее точная копия исходного массива).

Внизу мы рассмотрим пример, где мы скопируем массив, а затем посмотрим на адрес, по которому хранится значение:

package main

import "fmt"

func main() {
var initArray = [...]int{1, 2, 3}
var copyArray = initArray

fmt.Printf("Address of initArray: %p\n", &initArray)
fmt.Printf("Address of copyArray: %p\n", &copyArray)
}

/*
Output:
Address of initArray: 0xc00001a018
Address of copyArray: 0xc00001a030
*/

Слайсы

Слайсы в Go более гибкие, они позволяют изменять свою длину. По сути слайсы являются надмножеством массивов. Слайсы создают нам массив, которым мы можем пользоваться как обычным массивом и при надобности расширяют его.

Слайсы можно создать двумя способами:

// С помощью make
var foo []byte
s = make([]byte, 5, 5)

// С помощью shorthand syntax
bar := []byte{}

Способ с make

Способ с make является более интересным, так как дает нам возможность задать тип, длину и вместимость.

С типом я думаю никаких проблем быть не должно. Тип слайса формируется в виде []тип.

С длиной тоже ничего интересного. В зависимости от введенного количества - массив заполнится нулевыми значениями, например:

package main

import "fmt"

func main() {
var foo = make([]byte, 5)
var bar = make([]int, 10)
var fee = make([]string, 2)

fmt.Println(foo, bar, fee)
}

/*
Output:
[0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0] [ ]
*/

Последний параметр - вместимость играет важную роль в производительности программы, а также является интересным. По сути он говорит о том сколько памяти нужно выделить заранее под наш массив, чтобы при расширении нам не пришлось искать новый участок памяти.

Например, если мы создадим массив с вместимостью в 10 элементов, наполним его 5-ю элементами, а потом добавим один - адрес массива не изменится:

package main

import "fmt"

func main() {
var foo = make([]int, 5, 10)
fmt.Printf("Address of foo array [before append]: %p\n", &foo)

foo = append(foo, 222)
fmt.Printf("Address of foo array [after append]: %p\n", &foo)
}

/*
Output:
Address of foo array [before append]: 0xc0000aa018
Address of foo array [after append]: 0xc0000aa018
*/

К слову, если мы явно не задали вместимость слайса (то есть использовали конструкцию make([]int, 5)), то вместимость будет равна длине массива (в данном случае - 5).

Если же мы укажем вместимость массива меньше, чем его длину, то код и вовсе нескомпилируется:

package main

import "fmt"

func main() {
var foo = make([]int, 5, 4)
fmt.Printf("Capacity of the array: ", cap(foo))
}

/*
./prog.go:6:24: invalid argument: length and capacity swapped
*/

Что будет если мы переполним вместимость?

Если же мы переполним вместимость слайса, то вместимость умножится на 2:

package main

import "fmt"

func main() {
var foo = make([]int, 10, 10) // Изначальная вместимость - 10
foo = append(foo, 2) // Добавляем элемент
fmt.Println("Length of the array:", len(foo))
fmt.Println("Capacity of the array:", cap(foo))
}

/*
Output:
Length of the array: 11
Capacity of the array: 20
*/

При этом в памяти произойдет следующее:

Go понимает что нам не хватает памяти и посмотрит есть ли после текущего сегмента памяти еще столько же ячеек;

Если ячейки есть, он не будет передвигать массив и просто зарезервирует больше памяти;

Если ячеек нет, то он скопирует всю информацию из уже использующегося сегмента и найдет вдвое больше свободных ячеек, после нажождения он перенесет туда все данные и отдаст нам адрес сегмента;

Shorthand-syntax

С короткой версией объявления слайса все проще:

package main

import "fmt"

func main() {
foo := []int{1, 2, 3}
fmt.Println("Length of the array:", len(foo))
fmt.Println("Capacity of the array:", cap(foo))
}

/*
Output:
Length of the array: 3
Capacity of the array: 3
*/

В примере вверху Go создаст массив (под капотом) с длиной в три ячейки и такой же вместимостью.

Срезы на слайсах

Срезом на слайсе является дочерний слайс, который ссылется только на часть слайса:

package main

import "fmt"

func main() {
name := []string{"D", "a", "n", "i", "i", "l"}
firstThreeLetters := name[:3]
fmt.Println(firstThreeLetters)
}

/*
Output:
[D a n]
*/

Не смотря на то, что слайс и срез - понятия взаимозаменяемые (а если быть точнее, то срез - перевод от англ. slice), мы будем называть слайсами все новосозданные слайсы с помощью make() или shorthand-синтаксиса, а срезами будем называть слайсы проделанные над уже существующим массивом.

Мы также можем делать срезы на массивах, таким образом мы можем делать массивы динамически расширяемыми:

package main

import "fmt"

func main() {
nameArray := [6]string{"D", "a", "n", "i", "i", "l"}
nameSlice := nameArray[:]
nameSlice = append(nameSlice, "!")
fmt.Println(nameSlice)
}

/*
Output:
[D a n i i l !]
*/

Слайс под капотом

Слайс под капотом является структурой, которая содержит ссылку на исходный массив, длину и вместимость:

struct {
array *[]T
length int
capacity int
}

Когда мы создаем новый слайс или срезаем массив, то ссылка массива присвивается полю array, с помощью данного указателя слайс сможет обращаться к массиву под капотом. length и capacity хранят длину и вместимость, соответственно.

Поскольку слайс ссылается на часть массива, мы можем срезать часть массива. Срез не копирует элементы массива, он просто ссылается на них. Таким образом при изменении среза, изменится и массив, с которого мы брали срез:

package main

import "fmt"

func main() {
nameArray := [6]string{"D", "a", "n", "i", "i", "l"}
nameSlice := nameArray[:3]
nameSlice[len(nameSlice) - 1] = "m"
fmt.Println(nameSlice) // [D a m]
fmt.Println(nameArray) // [D a m i i l]
}

Мы также можем сделать так, чтобы срез занял всю длину исходного массива. Так как слайс хранит вместимость исходного массива - мы можем сделать срез снова и указать параметр cap(nameArray):

package main

import "fmt"

func main() {
nameArray := [6]string{"D", "a", "n", "i", "i", "l"}
nameSlice := nameArray[:3]
nameSlice[len(nameSlice)-1] = "m"
fmt.Println(nameSlice) // [D a m]

// Делаем новый срез
nameSlice = nameSlice[0:cap(nameSlice)]
fmt.Println(nameSlice) // [D a m i i l]
}

У вас может возникнуть вопрос: почему мы не срезали cap(nameSlice) - 1, ибо мы указали в конце несуществующий индекс (на один больше, нежели существует в массиве). Все дело в том, что последний элемент при срезе не включается в срез.

То есть, первый индекс идет включительно в срез, а последний - не включительно.

Копирование

Как уже можно понять, при срезе с массива или слайса мы не создаем новый слайс. Также, если мы присвоим одной переменной значение слайса другой переменной - они обе будут указывать на один массив:

package main

import (
"fmt"
)

func main() {
nameSlice := []string{"D", "a", "n", "i", "i", "l"}
secondNameSlice := nameSlice
secondNameSlice[0] = "T"
fmt.Println(nameSlice, secondNameSlice) // [T a n i i l] [T a n i i l]
}

Мы можем избежать такого поведения с помощью копирования. Для того чтобы скопировать слайс (создать независимую копию) - нам достаточно использовать функцию copy:

package main

import (
"fmt"
)

func main() {
nameSlice := []string{"D", "a", "n", "i", "i", "l"}
secondNameSlice := make([]string, len(nameSlice), cap(nameSlice))
copy(secondNameSlice, nameSlice)
secondNameSlice[0] = "T"

fmt.Println(nameSlice, secondNameSlice) // [D a n i i l] [T a n i i l]
}

сopy и append под капотом

Мы можем заметить два различия: при использовании функции append - мы переприсваивали значение переменной:

foo := []int {}
foo = append(foo, 1)

В случае с copy мы просто передаем саму переменную (не ссылку, а именно переменную!):

foo := []int {1, 2}
bar := []int {}
copy(bar, foo)

Вот как работает копирование под капотом:

func copy(to []T, from []T) {
for i := range from {
to[i] = from[i]
}
}

Разработчики Go решили не добавлять часть с инициализацией нового слайса внутрь copy.

В случае с дополнением все немного иначе. Тут разработчики Go посчитали, что функция сама должна решать нужно ли инициализировать новый слайс, или можно дополнить данные в уже существующий слайс:

func append(slice []T, data ...T) []T {
initialLength := len(slice)
finalLength := m + len(data)
if finalLength > cap(slice) { // if necessary, reallocate
// allocate double what's needed, for future growth.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:finalLength]
copy(slice[initialLength:finalLength], data)
return slice
}

Вместо заключения 🌚

Если вам понравилась данная статья - то вы всегда можете перейти в мой блог, там больше схожей информации о веб-разработке.

Если у вас остались вопросы - не стесняйтесь задавать их в комментариях. Хорошего времяпрепровождения! 💁🏻‍♂

Теги:

go

array

memory

Хабы:

Go

+9

109

14
+14

20

Карма

0

Рейтинг

Даниил Шило @crackidocky

Fullstack Engineer (Vue/Typescript/Go)

Подписаться

Комментарии 14
+14

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.1K

Кейс

+40

36

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.6K

Кейс

Перевод

+27

19

2
+2

lokosst 12 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

23

0

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.3K

Мнение

+19

17

59
+59

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.1K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.6K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.5K

+16

5

6
+6

psygalaxy 13 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.7K

Обзор

+16

29

9
+9

kubelet 16 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

22

1
+1

Исследование: как и где ML-специалисты используют ИИ. Расскажите о своем опыте
Опрос

Показать еще

Минуточку внимания

Турбо

Полный свайп: оцени, подходит ли тебе работа в IT-ритейле

Промо

Неважно, что ты выберешь — с Промокодусом будет дешевле

Событие

Хабр приглашает маркетологов и пиарщиков на тест-драйв

Вопросы и ответы

Trace/breakpoint trap (core dumped)?
LinuxПростой2 ответа

Неточность формулировки в книге или же я не понимаю?
GoПростой2 ответа

Зачем for {select} оборачивать в еще один select с time.Ticker?
GoПростой3 ответа

Какие гарантии на выравнивание адреса указателя?
GoСредний1 ответ

Как поменять env переменную PATH в golang (MacOS)?
GoСредний2 ответа

Больше вопросов на Хабр Q&A

Читают сейчас

Как превратить ноутбук в полноценное рабочее место не хуже стационарного ПК

12K

71
+71

Нашел ЛЯП в игре 5букв от Т-банка, который позволяет со 100% вероятностью угадать скрытое слово

54K

115
+115

Как выйти из IT?.. и пойти слесарем на завод. Моя попыточка дауншифтинга [4\5]

1.3K

11
+11

Моё мини-исследование про продолжительность жизни в России

6.3K

59
+59

Роскомнадзор направил операторам связи РФ письма о необходимости удаления ПО Speedtest Ookla из своей IT-инфраструктуры

21K

100
+100

Исследование: как и где ML-специалисты используют ИИ. Расскажите о своем опыте
Опрос

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Ближайшие события

7 августа

Мем-пати: как построить IT-бренд через вирусный контент и юмор

Москва

Больше событий в календаре

Маркетинг

13 августа

Как повысить эффективность врача в стоматологии на 25% с помощью ИИ?

Онлайн

Больше событий в календаре

Менеджмент

13 августа

Тест-драйв Хабра для маркетологов и пиарщиков

Москва

Больше событий в календаре

Маркетинг

16 августа

«ИТ-летник» — конференция от Совкомбанк Технологий

Казань

Больше событий в календаре

Разработка

Аналитика

3 сентября

«GoCloud Tech 2025» — IT-конференция про облака и AI

Москва • Онлайн

Больше событий в календаре

Разработка

18 – 19 сентября

Конференция по корпоративному онлайн-обучению iSpring Days 2025

Москва • Онлайн

Больше событий в календаре

Администрирование

Менеджмент

Другое

19 сентября

Премия для технологических стартапов Startech.Awards 2025

Москва • Онлайн

Больше событий в календаре

Другое

30 сентября

VII премия в сфере фармацевтического маркетинга и креатива MedMen Awards 2025

Онлайн

Больше событий в календаре

Маркетинг

Другое

30 октября

Премия «Проксима»

Москва

Больше событий в календаре

Маркетинг

Реклама

АО СЕЛЕКТЕЛ

ИНН 7810962785

erid: 2VSb5zDBFAH

1 000 000 стартапам

Кешбэк 30% на год в Selectel!

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr